高级语言怎么来的(三):FORTRAN 语言是怎么来的

#Technomous #PL

FORTRAN 语言是怎么来的

在高级语言是怎么来的子系列的高级语言怎么来的(一):一切从递归开始中,我们结合当时硬件的特点,分析了 FORTRAN 为什么一开始不支持递归。但是 FORTRAN 本身是怎么来的这个问题其实还是没有得到正面回答,本节我们就谈谈 FORTRAN 语言本身是怎么来的。

其实,FORTRAN 语言也是现实驱动的。所以我们还是回到当时,看看当时程序员的需求和软硬件条件,看看 FORTRAN 是怎么来的。了解历史的另一个好处是,因为 FORTRAN 的发展历史正好和高级语言的发展历史高度重合,所以了解 FORTRAN 的背景,对于理解其他高级语言的产生都是大有帮助的。

困难的浮点计算

我们先从硬件的角度说起。大致从 1946 年第一台计算机诞生,到 1953 年,计算机一直都缺少两件非常重要的功能,一个叫浮点计算,一个叫数组下标寻址,这两个功能的缺失直接导致了高级语言的兴起。我们依次单个分析。读者对浮点计算应该都不陌生,用通俗的话说就是如 0.98×12.6 这样的实数乘法,或者 0.98 + 12.6 这样的实数加法的运算。用行话说,就是用计算机进行大范围高精度数的算术运算。

学过二进制的同学都知道,二进制整数之间的乘法和加法等运算都是相对简单的,和正常的十进制运算是一样的,只是把加法和乘法这些基本操作用更加简单的逻辑或(OR)和 逻辑与(AND)实现而已,在电子电路上也很好实现。因此,就是世界上最早的电子计算机,ENIAC,也是支持整数的乘法加法等算术操作的。

可是浮点运算就不一样了。因为一个额外的小数点的引入,在任何时候都要注意小数点的对齐。如果用定点计数,则计数的范围受到限制,不能表示非常大或者非常小的数。所以,浮点数一般都是用科学记数法表示的,比如 IEEE 754 标准。(不熟悉 IEEE 754 的读者也可以想像一下如何设计一套高效的存储和操作浮点数的规范和标准,以及浮点算法),科学记数法表示的浮点数的加减法每次都要对齐小数点,乘除法为了保持精度,在设计算法上也有很多技巧,所以说,相比较于整数的运算和逻辑运算,浮点运算是一件复杂的事情。落实到硬件上就是说,在硬件上设计一个浮点运算,需要复杂的电路和大量的电子元器件。在早期电子管计算机中,是很少能做到这么大的集成度的。因此,不支持浮点也是自然的设计取舍。在计算机上放一个浮点模块这个想法,需要等电子工业继续发展,使得电子管体积小一点,功耗低一点后,才能进入实践。

关于浮点计算的一些八卦

关于浮点,这里顺带八卦一点浮点计算的事情。在计算机芯片设计中,浮点计算一直是一个让硬件工程师头疼的事情,即使到了 386 时代,386 处理器(CPU)的浮点乘法也是用软件模拟的,如果想用硬件做浮点乘法,需要额外购买一块 80387 浮点协处理器 FPU,否则就在 386 上做软件的模拟。这样做的原因在一块硅片上刻蚀一个 CPU 和一个FPU 需要的集成度还是太高,当时的工艺根本没法实现。真的把 FPU 和 CPU 融在一起刻蚀到一块硅片上,已经是 1989 年的事情了。当时,Intel 把融合了 80386 和 80387 的芯片改了改,起了个名字叫 80486,推向了市场。带着浮点的处理器的普及,使得个人计算机能做的事情变多了。极度依赖于浮点计算的多媒体计算机(视频和声音等多媒体的压缩,转换和回放都是要依赖于浮点运算的),也正好随着 80486 的流行,逐渐普及开来。

在处理器上融合浮点运算依然是困难的。即使到今天,很多低端的处理器,都不带有浮点处理器。所以,号称能够上天入地的,被移植到很多低端设备比如手机上的 Linux 内核,必然是不能支持浮点运算的,因为这样会破坏内核的可移植性。我们都知道,在内核模式下,为了保证内核操作的原子性,一般在内核从事关键任务的时候所有中断是要被屏蔽的,用通俗的话说就是内核在做事情的时候,其他任何人不得打扰。如果内核支持浮点运算,不管是硬件实现也好,软件模拟也罢,如果允许在内核中进行像浮点计算这样复杂而耗时的操作,整个系统的性能和实时响应能力会急剧下降。即使是在硬件上实现的浮点运算,也不是件容易的事情,会耗费 CPU 较多的时钟周期,比如 Pentium 上的浮点数除法,需要耗费 39 个时钟周期才行,在流水线设计的CPU中,这种占用多个时钟周期的浮点运算会让整个流水线暂停,让CPU的吞吐量下降。在现代 CPU 设计中,工程师们发明了超标量,乱序执行,SIMD 等多种方式来克服流水线被浮点运算这种长周期指令堵塞的问题,这都是后话了。

正因为对于计算机来说,浮点运算是一个挑战性的操作,但又是做科学计算所需要的基本操作,所以浮点计算能力就成了计算机能力的一个测试标准。我们常常听说有一个世界上前 500 台最快的超级计算机列表,这里所谓的“快”的衡量标准,就是以每秒钟进行多少次浮点计算(FLOPS)为准。按照 Top500.org,即评选世界上前 500 台超级计算机的机构 2009 年 6 月的数据,世界上最快的计算机,部署在美国能源部位于新墨西哥的洛斯阿拉莫斯国家实验室(Los Alamos National Laboratory),当年造出第一颗原子弹的实验室。这台超级计算机,浮点计算速度的峰值高达 1456 TFlops,主要用来模拟核试验。因为美国的所有核弹头,海军核动力航母中的反应堆以及核试验,都由能源部国家核安全署(NNSA)管理,所以能源部一直在投资用超级计算机进行核试验。在 1996 年美国宣布不再进行大规模的物理核试验后的这么多年,美国能源部一直用超级计算机来做核试验,所以在 Top500 列表中,美国能源部拥有最多数量的超级计算机。

数组下标寻址之障

言归正传,我们刚才说了在早期计算机发展史上,浮点计算的困难。除了浮点计算,还有一件事情特别困难,叫做数组下标寻址。用现代通俗的话说,就是当年的计算机,不直接支持 A[3] 这样的数组索引操作,即使这个操作从逻辑上说很简单:把数组 A 的地址加上 3,就得到了 A[3] 的地址,然后去访问这个地址。

这个困难在今天的程序员看来是不可思议的。为什么这么简单的数组下标寻址机制最一开始的计算机没有支持呢?原来,当年的计算机内存很小,只有一千到两千的存储空间,所以,描述地址只需要几位二/十进制数(BCD)。从而,在每条指令后面直接加一个物理地址是可行且高效的寻址方式。这种寻址方式,叫做直接寻址,当时所有的机器,都只支持直接寻址,因为在机器码中直接指出操作数的准确地址是最简单直接的方法,计算机不需要任何复杂的地址解码电路。但坏处是,这个设计太不灵活了,比如说 A[3] 这个例子,就没法用直接寻址来表示。

一般情况下,如果知道数组A, 对于 A[3] 这样的例子,用直接寻址问题去模拟间接寻址的困难还不是很大,只要程序员事先记住数组 A 的地址然后手工加上 3 就行了(A也是程序员分配的,因为当时没有操作系统,所以程序员手工管理内存的一切)。可是,也有一些情况这样直接寻址是不行的。比如说,当时计算机已经能支持跳转和判断指令了,也就是说,可以写循环语句了。我们可以很容易看到,以 i 为循环变量的循环体内,对 A[i] 的访问是不能写成一个静态的直接寻址的,因为 i 一直在变化,所以不可能事先一劳永逸的定好 A[i] 的所在位置,然后静态写在程序中。

这样,即使写一个简单的 10×10 矩阵的乘法,程序员就不得不死写 10 的三次方即 1000 行地址访问,而没办法用几行循环代替。当时的一些聪明人,也想了一些方法去克服这个问题,比如说,他们先取出 A 的地址,然后做一次加法,把结果,也就是当时 A[i] 的地址,注射到一个读内存的 LOAD 指令后面。然后执行那条 LOAD 指令。比如我要读 A[i],我先看,A的地址是 600,再看看 i 是 3, 就加上 i,变成 603,然后,把后面的指令改成 LOAD 603,这样,就可以读到 A[i]。这个小技巧之所以可行,要感谢冯诺依曼爷爷的体系设计。在冯诺依曼计算机中,数据和程序是混在一起不加区分的,所以程序员可以随时像修改数据一样修改将要运行的下一条程序指令。就这样,靠着这个小技巧,好歹程序员再也不要用 1000 行代码表示一个矩阵乘法了。

SpeedCoding 的出现

计算机本来就是用来做数学计算的,可是科学计算里面最最基本的两个要素–浮点计算和数组下标访问,在当时的计算机上都缺少支持。这种需求和实际的巨大落差,必然会召唤出一个中间层来消弭这种落差。其实计算机科学的一般规律就是这样:当 A 和 C 相差巨大的时候,我们就引入一个中间层 B,用 B 来弥合 A 和 C 之间的不兼容。当年的这个中间层,就叫做 SpeedCoding,由 IBM 的工程师 John Backus 开发。

SpeedCoding,顾名思义,就是让程序员编程更快。它其实是一个简单,运行在 IBM 701 计算机上的解释器。它允许程序员直接写浮点计算和下标寻址的指令,并且在底层把这些 “伪指令” 翻译成对应的机器码,用软件模拟浮点计算,自动修改地址等等。这样,程序员就可以从没完没了的手工实现浮点运算和下标寻址实现中解放出来,快速的编程。这个 SpeedCoding,这可以算得上是 FORTRAN 的种子了。

虽然这个解释器超级慢,程序员用这个解释器也用得很爽,也不感到它非常慢。这是因为当年计算机浮点计算都绕不过软件模拟,即使最好的程序员用机器码而不用这个解释器,写出来的程序,也不比这个解释器下运行快多少。另一个更加重要的原因是,这个解释器极大的减少了程序员 debug 和 code 的时间。随着计算机速度的提高,当年一个程序耗费的计算成本和程序员编程耗费的人力成本基本上已经持平了,所以,相比较于写更加底层的机器码,用了 SpeedCoding 的程序员的程序虽然慢点,但人力成本瞬间降成 0,总体下来,用 SpeedCoding 比起不用来,总体成本还要低不少。

好景不长,因为客户一直的要求和电子工业的发展,IBM 在 1954 年,终于发布了划时代的 704 计算机,很多经典的语言和程序,都首次在 704 上完成了。比如之前我们在本系列的高级语言怎么来的(一):一切从递归开始篇中提到的 Steve Russell 的 LISP 解释器,就是在 704 上完成的。704 计算机一下子支持了浮点计算和间接下标寻址。这下用 SpeedCoding 的人没优势了,因为机器码支持浮点和下标寻址之后,写机器码比写SpeedCoding 复杂不了多少,但是速度快了很多倍,因为 SpeedCoding 解释器太慢了,以前因为浮点和解释器一样慢,所以大家不在意它慢,现在浮点和寻址快了,就剩下解释器慢,写机器码的反而占了上风,程序员也就不用 SpeedCoding 了。

FORTRAN 创世纪

在 704 出来之前,做 SpeedCoding 的 John Backus 就认识到,要想让大家用他的 SpeedCoding,或者说,想要从软件工具上入手,减少程序的开发成本,只有两个方法:1. 程序员可以方便的写数学公式 2. 这个系统最后能够解析/生成足够的快的程序。他认为,只有达到了这两点,程序员才会乐意使用高级的像 SpeedCoding 这样的工具,而不是随着硬件的发展在机器码和 SpeedCoding 这样的工具之间跳来跳去。他本人通过实现 SpeedCoding,也认识到如果有一个比机器码高级的语言,生产效率会高很多倍。那么,现在唯一的问题就是实现它,当然,这就不是一个小项目了,就需要 IBM 来支持他的开发了。所以,在 1953年,他把他的想法写成了一个文档,送给了 IBM 的经理。项目在 1954 年,704 发布的当年,终于启动。John Backus 领导的设计一个能达到上面两点的编程系统的项目的成果,就是日后的 FORTRAN。

和现在大多数编程语言不一样,FORTRAN 语言的设计的主要问题不是语法和功能,而是编译器怎么写才能高性能。John Backus 日后回忆说,当时谁也没把精力放在语言细节上,语言设计很潦草的就完成了(所以其后正式发布后又经过了N多修订),他们所有的功夫都是花在怎么写一个高性能的编译器上。这个高性能的编译器很难写,到 1957 年才写好,总共花了 IBM 216 个人月。等到 FORTRAN 一推出,不到一年的时间,在 IBM 总共售出的 60 台 704上,就部署了超过一半。现在没啥编程语言能够这么牛的攻城掠地了 :)

结语

放到历史的上下文中看,FORTRAN 的出现是很自然的。一方面,复杂的数学运算使得一个能够表述数学计算的高级语言成为必须,计算机的发展也为这个需求提供的硬件条件;另一方面,随着计算机的发展,程序员的时间成本一直不变,但是计算的成本一直在降低,用高级语言和用机器码在性能上的些许差异变得可以忽略。这样的历史现实,必然会召唤出以少量的增加计算机工作量为代价,但能大幅度降低程序员时间成本的新的工具和设计。这种新的工具,新的设计,又对程序设计产生革命性的影响。在整个编程发展的历史上,FORTRAN 和其他高级语言的出现可以说是第一波的革命;而后,UNIX 和 C 语言的兴盛,使得系统编程的效率得到革命性提升,可以算是第二波革命;而面向对象方法,使得复杂的 GUI 等系统的编程效率得到提升,应该算得上是第三波革命。到如今,现在各种各样的方法论就更加多了,且看以后回看,哪种方法和工具能够大浪淘沙留下来。